package dolphin.mt521.cam.ac.uk;

import java.io.ByteArrayOutputStream;
import java.util.Arrays;

import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.transform.DftNormalization;
import org.apache.commons.math3.transform.FastFourierTransformer;
import org.apache.commons.math3.transform.TransformType;

import android.util.Log;

public class Decode {
    
    private static double backgroundLevel = 0.0;
    private static int chunkSize = Encode.getChunkSize();
    private static int freqIndex = 0;
    private static int spacing = Encode.getSpacing();
    private static int sampleRate = (int)Encode.getSampleRate();
    
    public static void decode(byte[] bTone) {
        int halfChunkSize = chunkSize/2;
        
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        double[] dTone = new double[halfChunkSize];
        double[] dTone2 = new double[halfChunkSize];        //for second chunk comparison
        double[] transformed = new double[halfChunkSize];
        double[] transformed2 = new double[halfChunkSize];  //for second chunk comparison
        long lFreq;
        byte bFreq;
        Log.i("Dolphin.decode","Sound Captured");
        backgroundLevel = getBackground(bTone);
        
        //scan array until find data, then measure 2 chunks of half the data duration. Note whichever
        //is strongest and skip all subsequent weak chunks by adding 2*halfchunk to the incrementor.
        boolean noData = true;
        boolean chunk = true;  //which chunk to measure from now on? true if chunk1, false if chunk2
        int inc = chunkSize;   //used first chunk for background measure, can't be data
        while(noData) {
            for(int j = 0; j<halfChunkSize; j++) {
                dTone[j] = (double) bTone[inc+j];
            }
            transformed = Decode.fft(dTone);
            if (isData(transformed)) {
                for(int j = 0; j<halfChunkSize; j++) {
                    dTone2[j] = (double) bTone[inc+j+halfChunkSize];    //get next chunk
                }
                transformed2 = Decode.fft(dTone2);
                if(transformed[freqIndex]>transformed2[freqIndex]) {    //which amplitude at that point was stronger?
                    lFreq = (Decode.getFrequency(transformed)-300)/spacing;
                    bFreq = lFreq>127 ? (byte) (lFreq-256) : (byte) lFreq;
                    stream.write(bFreq);
                    
                }
                else {
                    lFreq = (Decode.getFrequency(transformed2)-300)/spacing;
                    bFreq = lFreq>127 ? (byte) (lFreq-256) : (byte) lFreq;
                    stream.write(bFreq);
                    chunk=false;                                        //measure second chunk from here onwards
                }
                noData = false;                                     //exit loop
            }
            else {
                inc += halfChunkSize;
            }
        }
        //line up incrementor for rest of array, need to start reading half chunks from the correct
        //space then increment by whole chunk
        if (chunk) {
            inc += chunkSize;
        }
        else {
            inc += chunkSize+halfChunkSize;
        }
        
        for(int i=inc; i<bTone.length; i+=chunkSize) { //possible exception? catch ArrayOutOfBounds but do nothing
            for(int j = 0; j<halfChunkSize; j++) {
                dTone[j] = (double) bTone[i+j];
            }
            transformed = Decode.fft(dTone);
            if (isData(transformed)){
                lFreq = (Decode.getFrequency(transformed)-300)/spacing;
                bFreq = lFreq>127 ? (byte) (lFreq-256) : (byte) lFreq;
                stream.write(bFreq);
            }
        }
        Log.i("Dolphin.decode","Sound Decoded");
        byte[] result = stream.toByteArray();
        Output.saveFile(result,"dolphin(half)");
        Log.i("Dolphin.decode","New file saved to dolphin.txt");
    }
    
    public static void decodeOld(byte[] bTone) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        double[] dTone = new double[chunkSize];
        double[] transformed = new double[chunkSize];
        long lFreq;
        byte bFreq;
        Log.i("Dolphin.decode","Sound Captured");
        backgroundLevel = getBackground(bTone);
        
        for(int i=chunkSize; i<bTone.length; i+=chunkSize) {    //first chunk used in comparison, required to never be data
            for(int j = 0; j<chunkSize; j++) {
                dTone[j] = (double) bTone[i+j];
            }
            transformed = Decode.fft(dTone);
            if (isData(transformed)){
                lFreq = (Decode.getFrequency(transformed)-300)/spacing;
                bFreq = lFreq>127 ? (byte) (lFreq-256) : (byte) lFreq;
                stream.write(bFreq);
            }
        }
        Log.i("Dolphin.decode","Sound Decoded");
        byte[] result = stream.toByteArray();
        Output.saveFile(result,"dolphin(full)");
        Log.i("Dolphin.decode","New file saved to dolphin.txt");
    }
    
    public static double[] fft(double[] input) {
        double[] result = new double[input.length];
        FastFourierTransformer transformer = new FastFourierTransformer(DftNormalization.STANDARD);
        try {
            Complex[] cArray = transformer.transform(input,TransformType.FORWARD);
            for (int i=0; i<cArray.length; i++) {
                double real = (cArray[i].getReal());
                double imag = (cArray[i].getImaginary());
                result[i] = Math.sqrt((real*real) + (imag*imag));
            }
        }
        catch (IllegalArgumentException e) {
            System.err.println("Illegal argument given to FFT in Decode.java");
            e.printStackTrace();
        }

        double[] toReturn = Arrays.copyOf(result,result.length/2);  //only look at the first half as the second is mirror image
        double dIndex = (double)toReturn.length*((double)300/(sampleRate/2));
        int ignoreIndex = (int) dIndex;
        Arrays.fill(toReturn,0,ignoreIndex,0.0d);                    //ignore first 300Hz, background noise overwhelms high freq
        return toReturn;
    }

    public static long getFrequency(double[] array) {
        int bin = 0;
        for (int i=1; i<array.length; i++) {
            bin = array[i]>array[bin] ? i : bin;
        }
        double frequency = ((double)(bin*Encode.getSampleRate()))/(array.length*2);
        long solution = spacing*(Math.round(frequency/spacing));
        
        //Consider range, not individual array
        if (solution==3390) {
            if (array[bin-5]>array[bin+5]) {
                solution=3360;
            }
        }
        else if (solution==2640) {
            if (array[bin-5]>array[bin+5]) {
                solution=2610;
            }
        }
        else if (solution==2310) {
            if (array[bin-5]>array[bin+5]) {
                solution=2290;
            }
        }
        else if (solution==5580) {
            if (array[bin-5]>array[bin+5]) {
                solution=5550;
            }
        }
        else if (solution==4800) {
            if (array[bin-5]>array[bin+5]) {
                solution=4770;
            }
        }
        else if (solution==7080) {
            if (array[bin-5]>array[bin+5]) {
                solution=7050;
            }
        }

        return solution;
    }

    //get the amplitude of the first 64msec (background noise, no data)
    public static double getBackground(byte[] bTone) {
        double max = 0.0;
        double[] dTone = new double[chunkSize];
        for(int j = 0; j<chunkSize; j++) {
            dTone[j] = (double) bTone[j];
        }
        double[] transformed = Decode.fft(dTone);       //is half the size as second half ignored
        for (int i=0; i<transformed.length; i++) {
            max = transformed[i]>max ? transformed[i] : max;
        }
        return max*1.2;      //allow a small tolerance, up to one fifth higher than the sample.
    }
    
    //check if this chunk of sound contains an encoded byte
    public static boolean isData(double[] amps) {
        double max = 0.0;
        for (int i=0; i<amps.length; i++) {
            if (amps[i]>max) {
                max = amps[i];
                freqIndex = i;
            }
        }
        if (max>backgroundLevel) {
            return true;
        }
        else {
            return false;
        }
    }
}